"""Configuration Synchronizer.

Handles bidirectional synchronization between local offline configurations
and remote servers with conflict detection, resolution, and incremental sync.
"""

import asyncio
import json
import logging
import time
import uuid
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any, Tuple, Set
from dataclasses import dataclass, field
from enum import Enum

import aiohttp
import hashlib

from .offline_manager import (
    OfflineConfigEntry, SyncOperation, ConflictEntry, ConfigStatus,
    NetworkStatus, OperationType, ConflictType
)
from ..utils.cache import LRUCache

logger = logging.getLogger(__name__)


class SyncDirection(Enum):
    """Synchronization direction."""
    UPLOAD = "upload"            # Local to remote
    DOWNLOAD = "download"        # Remote to local
    BIDIRECTIONAL = "bidirectional"  # Both directions


class SyncStrategy(Enum):
    """Synchronization strategy."""
    FULL_SYNC = "full_sync"              # Sync all configurations
    INCREMENTAL_SYNC = "incremental_sync" # Sync only changes
    CONFLICT_ONLY = "conflict_only"       # Sync only conflicts
    DEVICE_BASED = "device_based"         # Sync per device
    TIMESTAMP_BASED = "timestamp_based"   # Sync based on timestamps


class ConflictResolutionStrategy(Enum):
    """Conflict resolution strategy."""
    TIMESTAMP_PRIORITY = "timestamp_priority"  # Newer wins
    LOCAL_PRIORITY = "local_priority"          # Local always wins
    REMOTE_PRIORITY = "remote_priority"        # Remote always wins
    MANUAL_RESOLVE = "manual_resolve"          # User decides
    SMART_MERGE = "smart_merge"               # Intelligent merge


@dataclass
class SyncContext:
    """Synchronization context."""
    sync_id: str                         # Sync session ID
    strategy: SyncStrategy               # Sync strategy
    direction: SyncDirection             # Sync direction
    device_filter: Optional[List[str]] = None  # Device filter
    conflict_resolution: ConflictResolutionStrategy = ConflictResolutionStrategy.TIMESTAMP_PRIORITY
    
    # Progress tracking
    total_items: int = 0                 # Total items to sync
    processed_items: int = 0             # Processed items
    successful_items: int = 0            # Successfully synced items
    failed_items: int = 0                # Failed items
    conflicts_detected: int = 0          # Detected conflicts
    conflicts_resolved: int = 0          # Resolved conflicts
    
    # Timing
    start_time: Optional[datetime] = None
    end_time: Optional[datetime] = None
    
    # Results
    errors: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)
    sync_log: List[Dict[str, Any]] = field(default_factory=list)


@dataclass  
class RemoteConfigEntry:
    """Remote configuration entry."""
    entry_id: str                        # Entry ID
    device_id: str                       # Device ID
    config_data: Dict[str, Any]          # Configuration data
    version: str                         # Version
    last_modified: datetime              # Last modified time
    checksum: str                        # Content checksum
    metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass
class SyncDelta:
    """Synchronization delta."""
    to_upload: List[OfflineConfigEntry]      # Entries to upload
    to_download: List[RemoteConfigEntry]     # Entries to download
    conflicts: List[Tuple[OfflineConfigEntry, RemoteConfigEntry]]  # Conflicting entries
    identical: List[str]                     # Identical entries
    local_only: List[OfflineConfigEntry]    # Local only entries
    remote_only: List[RemoteConfigEntry]    # Remote only entries


class ConflictResolver:
    """Configuration conflict resolver."""
    
    def __init__(self):
        """Initialize conflict resolver."""
        self.resolution_strategies = {
            ConflictResolutionStrategy.TIMESTAMP_PRIORITY: self._resolve_by_timestamp,
            ConflictResolutionStrategy.LOCAL_PRIORITY: self._resolve_by_local_priority,
            ConflictResolutionStrategy.REMOTE_PRIORITY: self._resolve_by_remote_priority,
            ConflictResolutionStrategy.SMART_MERGE: self._resolve_by_smart_merge,
        }
        
        # Resolution rules for different field types
        self.field_rules = {
            'critical_fields': ['device_id', 'mac_address'],  # Never auto-resolve
            'array_merge_fields': ['tags', 'dependencies'],   # Merge arrays
            'timestamp_fields': ['created_at', 'updated_at'], # Use newer
            'numeric_fields': ['priority', 'attempts']        # Use higher
        }
    
    def detect_conflicts(self, local_entry: OfflineConfigEntry,
                        remote_entry: RemoteConfigEntry) -> List[ConflictEntry]:
        """Detect conflicts between local and remote entries."""
        conflicts = []
        
        try:
            # Compare configuration data
            local_data = local_entry.config_data
            remote_data = remote_entry.config_data
            
            # Deep comparison
            conflicts.extend(self._compare_nested_dict(
                local_data, remote_data, "",
                local_entry.device_id, local_entry.updated_at,
                remote_entry.last_modified
            ))
            
            # Compare metadata
            if local_entry.checksum != remote_entry.checksum:
                if not conflicts:  # No field-level conflicts but checksum differs
                    conflicts.append(ConflictEntry(
                        conflict_id=str(uuid.uuid4()),
                        device_id=local_entry.device_id,
                        field_path="__checksum__",
                        local_value=local_entry.checksum,
                        remote_value=remote_entry.checksum,
                        local_timestamp=local_entry.updated_at,
                        remote_timestamp=remote_entry.last_modified,
                        conflict_type=ConflictType.VALUE,
                        severity="high",
                        description="Configuration checksum mismatch"
                    ))
            
        except Exception as e:
            logger.error(f"Conflict detection error: {e}")
            conflicts.append(ConflictEntry(
                conflict_id=str(uuid.uuid4()),
                device_id=local_entry.device_id,
                field_path="__error__",
                local_value=str(e),
                remote_value="detection_failed",
                local_timestamp=local_entry.updated_at,
                remote_timestamp=remote_entry.last_modified,
                conflict_type=ConflictType.STRUCTURE,
                severity="high",
                description=f"Conflict detection failed: {e}"
            ))
        
        return conflicts
    
    def _compare_nested_dict(self, local_dict: Dict[str, Any], remote_dict: Dict[str, Any],
                           path: str, device_id: str, local_ts: datetime,
                           remote_ts: datetime) -> List[ConflictEntry]:
        """Compare nested dictionaries and detect conflicts."""
        conflicts = []
        all_keys = set(local_dict.keys()) | set(remote_dict.keys())
        
        for key in all_keys:
            current_path = f"{path}.{key}" if path else key
            
            if key not in local_dict:
                # Key only in remote
                conflicts.append(ConflictEntry(
                    conflict_id=str(uuid.uuid4()),
                    device_id=device_id,
                    field_path=current_path,
                    local_value=None,
                    remote_value=remote_dict[key],
                    local_timestamp=local_ts,
                    remote_timestamp=remote_ts,
                    conflict_type=ConflictType.STRUCTURE,
                    severity="medium",
                    description=f"Field {current_path} only exists in remote"
                ))
            elif key not in remote_dict:
                # Key only in local
                conflicts.append(ConflictEntry(
                    conflict_id=str(uuid.uuid4()),
                    device_id=device_id,
                    field_path=current_path,
                    local_value=local_dict[key],
                    remote_value=None,
                    local_timestamp=local_ts,
                    remote_timestamp=remote_ts,
                    conflict_type=ConflictType.STRUCTURE,
                    severity="medium",
                    description=f"Field {current_path} only exists locally"
                ))
            else:
                local_value = local_dict[key]
                remote_value = remote_dict[key]
                
                # Compare values
                if isinstance(local_value, dict) and isinstance(remote_value, dict):
                    # Recursive comparison for nested objects
                    conflicts.extend(self._compare_nested_dict(
                        local_value, remote_value, current_path,
                        device_id, local_ts, remote_ts
                    ))
                elif local_value != remote_value:
                    # Value conflict
                    severity = "high" if current_path in self.field_rules['critical_fields'] else "medium"
                    conflict_type = ConflictType.TYPE if type(local_value) != type(remote_value) else ConflictType.VALUE
                    
                    conflicts.append(ConflictEntry(
                        conflict_id=str(uuid.uuid4()),
                        device_id=device_id,
                        field_path=current_path,
                        local_value=local_value,
                        remote_value=remote_value,
                        local_timestamp=local_ts,
                        remote_timestamp=remote_ts,
                        conflict_type=conflict_type,
                        severity=severity,
                        description=f"Value conflict in {current_path}"
                    ))
        
        return conflicts
    
    async def resolve_conflict(self, conflict: ConflictEntry,
                             strategy: ConflictResolutionStrategy) -> ConflictEntry:
        """Resolve a single conflict using specified strategy."""
        if strategy == ConflictResolutionStrategy.MANUAL_RESOLVE:
            # Manual resolution not implemented in automated context
            conflict.resolution_strategy = strategy.value
            conflict.resolver = "pending_manual"
            return conflict
        
        resolver_func = self.resolution_strategies.get(strategy)
        if not resolver_func:
            raise ValueError(f"Unknown resolution strategy: {strategy}")
        
        try:
            resolved_value = resolver_func(conflict)
            conflict.resolved_value = resolved_value
            conflict.resolution_strategy = strategy.value
            conflict.resolution_timestamp = datetime.now()
            conflict.resolver = "auto"
            
        except Exception as e:
            logger.error(f"Conflict resolution error: {e}")
            conflict.resolution_strategy = "failed"
            conflict.resolver = f"error: {e}"
        
        return conflict
    
    def _resolve_by_timestamp(self, conflict: ConflictEntry) -> Any:
        """Resolve conflict by timestamp (newer wins)."""
        if conflict.local_timestamp > conflict.remote_timestamp:
            return conflict.local_value
        elif conflict.remote_timestamp > conflict.local_timestamp:
            return conflict.remote_value
        else:
            # Same timestamp, prefer local
            return conflict.local_value
    
    def _resolve_by_local_priority(self, conflict: ConflictEntry) -> Any:
        """Resolve conflict by local priority (local always wins)."""
        return conflict.local_value
    
    def _resolve_by_remote_priority(self, conflict: ConflictEntry) -> Any:
        """Resolve conflict by remote priority (remote always wins)."""
        return conflict.remote_value
    
    def _resolve_by_smart_merge(self, conflict: ConflictEntry) -> Any:
        """Resolve conflict by smart merging."""
        field_path = conflict.field_path
        
        # Array fields - merge unique values
        if field_path in self.field_rules['array_merge_fields']:
            if isinstance(conflict.local_value, list) and isinstance(conflict.remote_value, list):
                return list(set(conflict.local_value + conflict.remote_value))
        
        # Numeric fields - use higher value
        if field_path in self.field_rules['numeric_fields']:
            if isinstance(conflict.local_value, (int, float)) and isinstance(conflict.remote_value, (int, float)):
                return max(conflict.local_value, conflict.remote_value)
        
        # Timestamp fields - use newer
        if field_path in self.field_rules['timestamp_fields']:
            return self._resolve_by_timestamp(conflict)
        
        # Default to timestamp-based resolution
        return self._resolve_by_timestamp(conflict)


class ConfigSynchronizer:
    """Configuration synchronizer."""
    
    def __init__(self, config: Dict[str, Any]):
        """Initialize configuration synchronizer.
        
        Args:
            config: Synchronizer configuration
        """
        self.config = config
        self.base_url = config.get('remote_url', 'http://localhost:8080/api')
        self.timeout = config.get('timeout', 30)
        self.max_retries = config.get('max_retries', 3)
        self.batch_size = config.get('batch_size', 50)
        
        # Components
        self.conflict_resolver = ConflictResolver()
        
        # Active sync operations
        self.active_syncs: Dict[str, SyncContext] = {}
        
        # Result cache
        self.sync_cache = LRUCache(capacity=100, ttl=3600)  # 1 hour TTL
        
        logger.info("ConfigSynchronizer initialized")
    
    async def start_sync(self, strategy: SyncStrategy = SyncStrategy.INCREMENTAL_SYNC,
                        direction: SyncDirection = SyncDirection.BIDIRECTIONAL,
                        device_filter: Optional[List[str]] = None,
                        conflict_resolution: ConflictResolutionStrategy = ConflictResolutionStrategy.TIMESTAMP_PRIORITY
                        ) -> str:
        """Start synchronization operation.
        
        Args:
            strategy: Synchronization strategy
            direction: Sync direction
            device_filter: Optional device filter
            conflict_resolution: Conflict resolution strategy
            
        Returns:
            Sync operation ID
        """
        sync_id = str(uuid.uuid4())
        
        context = SyncContext(
            sync_id=sync_id,
            strategy=strategy,
            direction=direction,
            device_filter=device_filter,
            conflict_resolution=conflict_resolution,
            start_time=datetime.now()
        )
        
        self.active_syncs[sync_id] = context
        
        # Start sync in background
        asyncio.create_task(self._execute_sync(context))
        
        logger.info(f"Started sync operation: {sync_id}")
        return sync_id
    
    async def get_sync_status(self, sync_id: str) -> Optional[SyncContext]:
        """Get synchronization status.
        
        Args:
            sync_id: Sync operation ID
            
        Returns:
            Sync context or None if not found
        """
        return self.active_syncs.get(sync_id)
    
    async def cancel_sync(self, sync_id: str) -> bool:
        """Cancel active synchronization.
        
        Args:
            sync_id: Sync operation ID
            
        Returns:
            True if cancelled successfully
        """
        if sync_id in self.active_syncs:
            context = self.active_syncs[sync_id]
            context.errors.append("Cancelled by user")
            context.end_time = datetime.now()
            del self.active_syncs[sync_id]
            return True
        return False
    
    async def calculate_sync_delta(self, local_entries: List[OfflineConfigEntry],
                                 remote_entries: List[RemoteConfigEntry]
                                 ) -> SyncDelta:
        """Calculate synchronization delta.
        
        Args:
            local_entries: Local configuration entries
            remote_entries: Remote configuration entries
            
        Returns:
            Sync delta information
        """
        delta = SyncDelta(
            to_upload=[],
            to_download=[],
            conflicts=[],
            identical=[],
            local_only=[],
            remote_only=[]
        )
        
        # Create lookup maps
        local_map = {(e.device_id, e.entry_id): e for e in local_entries}
        remote_map = {(e.device_id, e.entry_id): e for e in remote_entries}
        
        all_keys = set(local_map.keys()) | set(remote_map.keys())
        
        for key in all_keys:
            device_id, entry_id = key
            
            if key in local_map and key in remote_map:
                local_entry = local_map[key]
                remote_entry = remote_map[key]
                
                # Compare checksums
                if local_entry.checksum == remote_entry.checksum:
                    delta.identical.append(entry_id)
                else:
                    # Potential conflict - need detailed analysis
                    conflicts = self.conflict_resolver.detect_conflicts(local_entry, remote_entry)
                    if conflicts:
                        delta.conflicts.append((local_entry, remote_entry))
                    else:
                        # Different but no conflicts detected, determine direction
                        if local_entry.updated_at > remote_entry.last_modified:
                            delta.to_upload.append(local_entry)
                        else:
                            delta.to_download.append(remote_entry)
            
            elif key in local_map:
                # Local only
                local_entry = local_map[key]
                delta.local_only.append(local_entry)
                delta.to_upload.append(local_entry)
            
            else:
                # Remote only
                remote_entry = remote_map[key]
                delta.remote_only.append(remote_entry)
                delta.to_download.append(remote_entry)
        
        return delta
    
    async def _execute_sync(self, context: SyncContext):
        """Execute synchronization operation."""
        try:
            logger.info(f"Executing sync {context.sync_id} with strategy {context.strategy.value}")
            
            # Get local and remote configurations
            if context.strategy == SyncStrategy.FULL_SYNC:
                local_entries, remote_entries = await self._get_full_sync_data(context)
            else:
                local_entries, remote_entries = await self._get_incremental_sync_data(context)
            
            context.total_items = len(local_entries) + len(remote_entries)
            
            # Calculate sync delta
            delta = await self.calculate_sync_delta(local_entries, remote_entries)
            
            # Process uploads
            if context.direction in [SyncDirection.UPLOAD, SyncDirection.BIDIRECTIONAL]:
                await self._process_uploads(context, delta.to_upload)
            
            # Process downloads
            if context.direction in [SyncDirection.DOWNLOAD, SyncDirection.BIDIRECTIONAL]:
                await self._process_downloads(context, delta.to_download)
            
            # Resolve conflicts
            if delta.conflicts:
                await self._resolve_conflicts(context, delta.conflicts)
            
            # Update statistics
            context.processed_items = context.total_items
            context.successful_items = context.processed_items - context.failed_items
            
            context.sync_log.append({
                'timestamp': datetime.now().isoformat(),
                'event': 'sync_completed',
                'total_items': context.total_items,
                'successful_items': context.successful_items,
                'failed_items': context.failed_items,
                'conflicts': context.conflicts_detected,
                'resolved': context.conflicts_resolved
            })
            
        except Exception as e:
            logger.error(f"Sync execution error: {e}")
            context.errors.append(str(e))
            context.failed_items = context.total_items
        
        finally:
            context.end_time = datetime.now()
            
            # Cache result
            self.sync_cache.put(context.sync_id, context)
            
            # Remove from active syncs
            if context.sync_id in self.active_syncs:
                del self.active_syncs[context.sync_id]
            
            logger.info(f"Sync {context.sync_id} completed")
    
    async def _get_full_sync_data(self, context: SyncContext
                                ) -> Tuple[List[OfflineConfigEntry], List[RemoteConfigEntry]]:
        """Get full synchronization data."""
        # This would integrate with the offline manager and remote API
        local_entries = []  # Get from offline storage
        remote_entries = []  # Get from remote API
        
        try:
            # Get remote configurations
            async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout)) as session:
                url = f"{self.base_url}/configs"
                params = {}
                
                if context.device_filter:
                    params['devices'] = ','.join(context.device_filter)
                
                async with session.get(url, params=params) as response:
                    if response.status == 200:
                        remote_data = await response.json()
                        remote_entries = [
                            RemoteConfigEntry(
                                entry_id=item['entry_id'],
                                device_id=item['device_id'],
                                config_data=item['config_data'],
                                version=item['version'],
                                last_modified=datetime.fromisoformat(item['last_modified']),
                                checksum=item['checksum'],
                                metadata=item.get('metadata', {})
                            )
                            for item in remote_data.get('configurations', [])
                        ]
        
        except Exception as e:
            logger.error(f"Error fetching remote data: {e}")
            context.errors.append(f"Remote fetch error: {e}")
        
        return local_entries, remote_entries
    
    async def _get_incremental_sync_data(self, context: SyncContext
                                       ) -> Tuple[List[OfflineConfigEntry], List[RemoteConfigEntry]]:
        """Get incremental synchronization data."""
        # Get only changed configurations since last sync
        # This would use timestamps or version numbers to determine changes
        
        local_entries = []  # Get changed local entries
        remote_entries = []  # Get changed remote entries
        
        return local_entries, remote_entries
    
    async def _process_uploads(self, context: SyncContext, entries: List[OfflineConfigEntry]):
        """Process configuration uploads."""
        if not entries:
            return
        
        logger.info(f"Uploading {len(entries)} configurations")
        
        # Process in batches
        for i in range(0, len(entries), self.batch_size):
            batch = entries[i:i + self.batch_size]
            
            try:
                await self._upload_batch(context, batch)
            except Exception as e:
                logger.error(f"Batch upload error: {e}")
                context.errors.append(f"Upload batch error: {e}")
                context.failed_items += len(batch)
    
    async def _upload_batch(self, context: SyncContext, entries: List[OfflineConfigEntry]):
        """Upload a batch of configurations."""
        upload_data = []
        
        for entry in entries:
            upload_data.append({
                'entry_id': entry.entry_id,
                'device_id': entry.device_id,
                'config_data': entry.config_data,
                'version': entry.local_version,
                'operation_type': entry.operation_type.value,
                'checksum': entry.checksum,
                'metadata': {
                    'source': entry.source,
                    'tags': entry.tags,
                    'dependencies': entry.dependencies
                }
            })
        
        async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout)) as session:
            url = f"{self.base_url}/configs/batch"
            
            async with session.post(url, json={'configurations': upload_data}) as response:
                if response.status == 200:
                    result = await response.json()
                    context.successful_items += result.get('success_count', 0)
                    context.failed_items += result.get('error_count', 0)
                    
                    if result.get('errors'):
                        context.errors.extend(result['errors'])
                else:
                    raise Exception(f"Upload failed with status {response.status}")
    
    async def _process_downloads(self, context: SyncContext, entries: List[RemoteConfigEntry]):
        """Process configuration downloads."""
        if not entries:
            return
        
        logger.info(f"Downloading {len(entries)} configurations")
        
        # Process in batches
        for i in range(0, len(entries), self.batch_size):
            batch = entries[i:i + self.batch_size]
            
            try:
                await self._download_batch(context, batch)
            except Exception as e:
                logger.error(f"Batch download error: {e}")
                context.errors.append(f"Download batch error: {e}")
                context.failed_items += len(batch)
    
    async def _download_batch(self, context: SyncContext, entries: List[RemoteConfigEntry]):
        """Download a batch of configurations."""
        # This would save remote configurations to local storage
        # For now, just simulate the process
        
        for entry in entries:
            try:
                # Convert to local format and save
                local_entry = OfflineConfigEntry(
                    entry_id=entry.entry_id,
                    device_id=entry.device_id,
                    config_data=entry.config_data,
                    operation_type=OperationType.UPDATE,
                    status=ConfigStatus.SYNCED,
                    local_version=entry.version,
                    remote_version=entry.version,
                    checksum=entry.checksum,
                    source="remote"
                )
                
                # Save to offline storage (would integrate with OfflineStorage)
                # await self.offline_storage.save_config_entry(local_entry)
                
                context.successful_items += 1
                
            except Exception as e:
                logger.error(f"Download entry error: {e}")
                context.failed_items += 1
                context.errors.append(f"Download {entry.entry_id}: {e}")
    
    async def _resolve_conflicts(self, context: SyncContext, 
                               conflicts: List[Tuple[OfflineConfigEntry, RemoteConfigEntry]]):
        """Resolve configuration conflicts."""
        if not conflicts:
            return
        
        logger.info(f"Resolving {len(conflicts)} conflicts")
        context.conflicts_detected = len(conflicts)
        
        for local_entry, remote_entry in conflicts:
            try:
                # Detect specific conflicts
                conflict_entries = self.conflict_resolver.detect_conflicts(local_entry, remote_entry)
                
                # Resolve each conflict
                for conflict in conflict_entries:
                    resolved_conflict = await self.conflict_resolver.resolve_conflict(
                        conflict, context.conflict_resolution
                    )
                    
                    if resolved_conflict.resolved_value is not None:
                        context.conflicts_resolved += 1
                    
                    context.sync_log.append({
                        'timestamp': datetime.now().isoformat(),
                        'event': 'conflict_resolved',
                        'device_id': conflict.device_id,
                        'field_path': conflict.field_path,
                        'strategy': resolved_conflict.resolution_strategy,
                        'resolver': resolved_conflict.resolver
                    })
                
            except Exception as e:
                logger.error(f"Conflict resolution error: {e}")
                context.errors.append(f"Conflict resolution: {e}")


# Example usage and testing
if __name__ == "__main__":
    import asyncio
    
    async def test_config_synchronizer():
        """Test configuration synchronizer."""
        config = {
            'remote_url': 'https://httpbin.org',  # Mock remote API
            'timeout': 30,
            'max_retries': 3,
            'batch_size': 10
        }
        
        synchronizer = ConfigSynchronizer(config)
        
        try:
            # Start a sync operation
            sync_id = await synchronizer.start_sync(
                strategy=SyncStrategy.INCREMENTAL_SYNC,
                direction=SyncDirection.BIDIRECTIONAL,
                conflict_resolution=ConflictResolutionStrategy.TIMESTAMP_PRIORITY
            )
            
            print(f"Started sync: {sync_id}")
            
            # Monitor progress
            for i in range(10):
                await asyncio.sleep(1)
                status = await synchronizer.get_sync_status(sync_id)
                
                if status:
                    print(f"Progress: {status.processed_items}/{status.total_items}")
                    if status.end_time:
                        print(f"Sync completed in {(status.end_time - status.start_time).total_seconds():.2f}s")
                        break
                else:
                    print("Sync completed")
                    break
            
        except Exception as e:
            print(f"Test error: {e}")
    
    # Run test
    asyncio.run(test_config_synchronizer())